﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Serialization;

namespace TacticalGridWars
{
    /// <summary>
    /// Holds and array of Tile objects that is used as the level in the game
    /// </summary>
    public class Grid
    {
        public enum TileState { Empty, Cover, Wall, Team1Spawn, Team2Spawn }
        public enum Direction { Up, Down, Left, Right, UpLeft, UpRight, DownLeft, DownRight, None }

        private List<Tuple<int, int>> que = new List<Tuple<int, int>>();
        private List<Tile> visited = new List<Tile>();

        #region Properties
        public Tile[,] Level { get; set; }
        public List<Spawn> Spawns { get; set; }
        public int Rows { get; set; }
        public int Columns { get; set; }
        public int TeamMembers { get; set; } 
        #endregion

        public Grid()
        {
            Spawns = new List<Spawn>();
            LoadLevel("Level0");
        }

        /// <summary>
        /// Loads the specified level into the Grid object.
        /// If the specified level does not exist it loads a dummy level.
        /// </summary>
        /// <param name="levelName">Filename of the level without extension</param>
        public void LoadLevel(string levelName)
        {
            SaveLevel saveLevel = new SaveLevel();
            XmlSerializer serializer = new XmlSerializer(typeof(SaveLevel));
            StreamReader file;
            try
            {
                file = new StreamReader("./Levels/" + levelName + ".xml");
                saveLevel = (SaveLevel)serializer.Deserialize(file);
            }
            catch
            {
                file = new StreamReader("./Levels/level0.xml");
                saveLevel = (SaveLevel)serializer.Deserialize(file);
            }
            file.Close();
            saveLevel.Load(this);
        }

        /// <summary>
        /// Clears all the tiles of reachable states: WalkReachable = false, SprintReachable = false
        /// </summary>
        public void ClearReachable()
        {
            for (int i = 0; i < Rows; i++)
            {
                for (int j = 0; j < Columns; j++)
                {
                    Level[i, j].WalkReachable = Level[i, j].SprintReachable = false;
                }
            }
        }

        /// <summary>
        /// Check the chance to hit betwen two points on the screen. Standard hitchance is 90%
        /// Walls reduce the hitchance to 0% and Cover to 50%.
        /// </summary>
        /// <param name="x">Origin x</param>
        /// <param name="y">Origin y</param>
        /// <param name="toX">Target x</param>
        /// <param name="toY">Target y</param>
        /// <returns>The hitchance in perfent as a double: 0 <= hitchance <= 1</returns>
        public double GetHitChance(double x, double y, double toX, double toY)
        {
            double xChange = 0;
            double yChange = 0;
            if (toX != x)
            {
                xChange = (toX - x) / 75;
            }
            if (toY != y)
            {
                yChange = (toY - y) / 75;
            }

            bool cover = false;
            while (!(Math.Abs(toX - x) < 20 && Math.Abs(toY - y) < 20))
            {
                //Level[(int)(y / 50), (int)(x / 50)].WalkReachable = true; //debugging
                if (Level[(int)(y / 50), (int)(x / 50)].State == TileState.Wall)
                {
                    return 0;
                }
                else if (Level[(int)(y / 50), (int)(x / 50)].State == TileState.Cover)
                {
                    cover = true;
                }
                x += xChange;
                y += yChange;
            }
            if (cover)
            {
                return 0.5;
            }
            else
            {
                return 0.9;
            }
        }

        /// <summary>
        /// Calculates what tiles the Soldier can reach using a Breadth-First-Algorithm
        /// </summary>
        /// <param name="s">The Soldier to get reachable tiles for</param>
        public void UpdateReachable(Soldier s)
        {
            int walkLimit;
            int sprintLimit;
            if (s.Moves > 1)
            {
                walkLimit = 4;
                sprintLimit = 2;
            }
            else
            {
                walkLimit = 2;
                sprintLimit = 0;
            }
            que.Clear();
            visited.Clear();
            Level[s.YPos, s.XPos].Moves = 0;
            Level[s.YPos, s.XPos].ParentDirection = Direction.None;
            visited.Add(Level[s.YPos, s.XPos]);
            que.Add(new Tuple<int, int>(s.XPos, s.YPos));
            while (que.Count != 0)
            {
                Direction[] dirs = GetDirsToVisit(Level[que.Last().Item2, que.Last().Item1].ParentDirection);
                foreach (var dir in dirs)
                {
                    VisitNeighbour(que.Last(), dir, walkLimit, sprintLimit);
                }
                que.Remove(que.Last());
            }
        }

        private void VisitNeighbour(Tuple<int, int> coords, Direction dir, int walkLimit, int sprintLimit)
        {
            int x;
            int y;
            switch (dir)
            {
                case Direction.Up: x = 0; y = -1; break;
                case Direction.Down: x = 0; y = 1; break;
                case Direction.Left: x = -1; y = 0; break;
                case Direction.Right: x = 1; y = 0; break;
                case Direction.UpLeft: x = -1; y = -1; break;
                case Direction.UpRight: x = 1; y = -1; break;
                case Direction.DownLeft: x = -1; y = 1; break;
                case Direction.DownRight: x = 1; y = 1; break;
                default: x = 0; y = 0; break;
            }

            x += coords.Item1;
            y += coords.Item2;

            if (x >= 0 && x < Columns && y >= 0 && y < Rows && !visited.Contains(Level[y, x]))
            {
                if (Level[y, x].Passable)
                {
                    Level[y, x].Moves = Level[coords.Item2, coords.Item1].Moves + 1;
                    Level[y, x].ParentDirection = dir;
                    if (Level[y, x].Moves < walkLimit)
                    {
                        Level[y, x].WalkReachable = true;
                    }
                    else if (Level[y, x].Moves < (walkLimit + sprintLimit))
                    {
                        Level[y, x].SprintReachable = true;
                    }
                    if (Level[y, x].Moves < (walkLimit + sprintLimit - 1))
                    {
                        que.Insert(0, new Tuple<int, int>(x, y));
                    }
                }
                visited.Add(Level[y, x]);
            }
        }

        private Direction[] GetDirsToVisit(Direction parentDir)
        {
            switch (parentDir)
            {
                case Direction.Up:
                    return new Direction[] { Direction.Up, Direction.UpLeft, Direction.UpRight };
                case Direction.Down:
                    return new Direction[] { Direction.Down, Direction.DownLeft, Direction.DownRight };
                case Direction.Left:
                    return new Direction[] { Direction.Left, Direction.UpLeft, Direction.DownLeft };
                case Direction.Right:
                    return new Direction[] { Direction.Right, Direction.UpRight, Direction.DownRight };
                case Direction.UpLeft:
                    return new Direction[] { Direction.Up, Direction.Left, Direction.UpLeft, Direction.UpRight, Direction.DownLeft };
                case Direction.UpRight:
                    return new Direction[] { Direction.Up, Direction.Right, Direction.UpLeft, Direction.UpRight, Direction.DownRight };
                case Direction.DownLeft:
                    return new Direction[] { Direction.Down, Direction.Left, Direction.UpLeft, Direction.DownLeft, Direction.DownRight };
                case Direction.DownRight:
                    return new Direction[] { Direction.Down, Direction.Right, Direction.UpRight, Direction.DownLeft, Direction.DownRight };
                default:
                    return new Direction[] { Direction.Up, Direction.Down, Direction.Left, Direction.Right, Direction.UpLeft, Direction.UpRight, Direction.DownLeft, Direction.DownRight };
            }
        }

        #region Tile-class
        /// <summary>
        /// Holds information about the state of a tile on the level
        /// </summary>
        public class Tile
        {
            #region Properties
            public TileState State { get; set; }
            public bool Reachable
            {
                get { return WalkReachable || SprintReachable; }
            }
            public bool WalkReachable { get; set; }
            public bool SprintReachable { get; set; }
            public bool Occupied { get; set; }
            public bool Passable 
            { 
                get 
                {
                    if (State != TileState.Cover && State != TileState.Wall && !Occupied)
                    {
                        return true;
                    }
                    else
                    {
                        return false;  
                    }
                } 
            }
            public int Moves { get; set; }
            public Direction ParentDirection { get; set; }
            #endregion Properties

            public Tile(TileState state = TileState.Empty)
            {
                this.State = state;
                WalkReachable = SprintReachable = Occupied = false;
            }
            public Tile()
            {
                State = TileState.Empty;
                WalkReachable = SprintReachable = Occupied = false;
            }
        } 
        #endregion

        #region Spawn-class
        /// <summary>
        /// Holds information about a spawnpoint for a Soldier
        /// </summary>
        public class Spawn
        {
            #region Properties
            public int Team { get; set; }
            public int XPos { get; set; }
            public int YPos { get; set; } 
            #endregion
            public Spawn()
            {
                Team = XPos = YPos = 0;
            }
            public Spawn(int team, int xPos, int yPos)
            {
                Team = team;
                XPos = xPos;
                YPos = yPos;
            }
        } 
        #endregion

        #region SaveLevel-class
        [Serializable()]
        [XmlType(AnonymousType = true)]
        [XmlRoot("SaveLevel")]
        public class SaveLevel
        {
            public int Rows = 0;
            public int Columns = 0;
            public int TeamMembers = 0;
            public List<Tile> Level = new List<Tile>();
            public List<Spawn> Spawns = new List<Spawn>();

            public void Load(Grid grid)
            {
                grid.Spawns.Clear();
                grid.Spawns.AddRange(Spawns);
                grid.Rows = Rows;
                grid.Columns = Columns;
                grid.Level = new Tile[Rows, Columns];
                grid.TeamMembers = TeamMembers;
                for (int i = 0; i < Rows; i++)
                {
                    for (int j = 0; j < Columns; j++)
                    {
                        grid.Level[i, j] = Level[i * Columns + j];
                    }
                }
            }
        } 
        #endregion
    }
}
